feat(rate-limits): per-realm configurable auth rate-limit ceilings#103
Merged
Conversation
The per-IP auth rate limiters (native-otp, magic-link, password-reset, email-otp, email-verification, passkey-begin, bootstrap) were hardcoded ASP.NET policies — the only way to raise a ceiling on a shared prod IdP was a code change + redeploy, which also changed it for every realm. Surfaced by AmZettel hitting the native-otp limit during normal iterative testing. Make each ceiling configurable per realm (limit + window), defaults UNCHANGED so a realm that never touches it behaves exactly as before. Reuses ADR-0011's RealmSettings cascade — the same pattern DCR's per-realm rate limits already use. How the live limiter reads per-realm config: the ASP.NET policy factories are synchronous and can't do the async settings lookup, so a thin middleware resolves the realm's AuthRateLimits (after RealmMiddleware, before UseRateLimiter) and stashes them on HttpContext.Items; the factories read the effective rule there, falling back to the shipped defaults. The realm slug + resolved limit are baked into the partition key so each realm gets its own per-IP bucket and a config edit applies on the next request. A short cache (TTL 0 in Testing) keeps the limiter's cheap-rejection property under flood. - Domain: AuthRateLimitSettings + AuthRateLimitPolicy + AuthRateLimitDefaults; nullable AuthRateLimits section on RealmSettings. - Application/service: read + update DTOs, patch (with validation) + map. - Api: AuthRateLimitResolutionMiddleware + AuthFixedWindow partition helper; the 7 per-IP policy factories now resolve their ceiling per request. - Frontend: a "Rate Limits" tab in Realm Settings (7 policies, limit + window) + German i18n. - Tests: lowered limit throttles sooner, raised limit allows past the old default; the existing boundary test still passes (defaults unchanged). - Docs: realm-settings.md "Rate Limits" section + native-apps.md pointer. Answers the AmZettel request (Atlas: requests-amzettel-native-otp-rate-limit-configurable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The per-IP auth rate limiters were hardcoded ASP.NET policies. Raising a ceiling on a shared production IdP meant a modgud code change + redeploy — and changed it for every realm. Surfaced when AmZettel hit the
native-otplimit during normal iterative deploy-and-verify testing.This makes each ceiling configurable per realm (limit + window), defaults unchanged so a realm that never touches it behaves exactly as before. Reuses the ADR-0011
RealmSettingscascade — the same pattern DCR's per-realm rate limits already use.Scope: the whole per-IP auth-limiter family —
native-otp,magic-link,password-reset,email-otp,email-verification,passkey-begin,bootstrap.How the live limiter reads per-realm config
The ASP.NET policy factories are synchronous and can't do the async settings lookup. So a thin middleware (
AuthRateLimitResolutionMiddleware) resolves the realm'sAuthRateLimits— afterRealmMiddlewareresolves the tenant, beforeUseRateLimiter— and stashes them onHttpContext.Items; the factories read the effective rule there, falling back to the shippedAuthRateLimitDefaults.Changes
AuthRateLimitSettings+AuthRateLimitPolicy+AuthRateLimitDefaults; nullableAuthRateLimitsonRealmSettings.AuthFixedWindowpartition helper; the 7 per-IP policy factories resolve their ceiling per request (theoauth-tokensliding/per-client policy is untouched).realm-settings.md→ Rate Limits section;native-apps.mdpointer.Behaviour note (worth a look in review)
Partitioning changes from pure-per-IP-global to per-realm-per-IP (the limit is now realm config, so the bucket must be per-realm for it to mean anything). On a shared IdP this means one IP gets a separate bucket per realm — slightly more lenient cross-realm, but coherent with per-realm limits. Defaults and single-realm behaviour are unchanged.
Answers the AmZettel request (Atlas:
requests-amzettel-native-otp-rate-limit-configurable).🤖 Generated with Claude Code